Skip to content

test: close the false-green mechanisms, wire the dead CI gates (+ coverage gap analysis) - #436

Merged
pftg merged 7 commits into
masterfrom
claude/test-coverage-analysis-5zm7nj
Aug 7, 2026
Merged

test: close the false-green mechanisms, wire the dead CI gates (+ coverage gap analysis)#436
pftg merged 7 commits into
masterfrom
claude/test-coverage-analysis-5zm7nj

Conversation

@pftg

@pftg pftg commented Aug 7, 2026

Copy link
Copy Markdown
Member

What

An audit of what the test suite actually covers, plus the fixes for every gap it found in the "coverage the suite claims but does not have" category.

rake test:unit: 272 → 275 runs, 5723 → 6096 assertions, 0 failures. Assertions up 373 while the test count went down by 13. That ratio is the point — the suite got smaller and started checking more.

Doc: docs/20-29-testing-qa/20.10-test-coverage-gap-analysis-reference.md.

The audit

lib/ is healthy at 91.6% line coverage (716/782 relevant lines, SimpleCov over the pure-Ruby tests). The risk was never in the Ruby. It was in the layers the suite treats as rendered output, and in gates that reported green without testing anything.

Fixed

1. The integration suite skipped itself when the build failed

hugo_pipeline_test.rb called skip when the Hugo build failed — the asset pipeline is exactly what a broken build takes down, so the one regression these tests exist to catch reported 0 failures with all 11 tests skipped.

Now build_failure + flunk, separating "hugo not on PATH" from a real build error and printing the last 30 lines of build output. Verified by shimming hugo to /bin/false.

2. Two CI gates existed but ran nowhere

  • rake test:html_proofer was invoked by no workflow, hook, or script. It now joins test:links in link-check.yml — as one rake invocation, because both default to the same OUTPUT_DIR and each triggers its own production build. build_for_linkcheck is memoized per process so the pair shares one build; two steps would build the site twice, the exact double-build that forced setup-hugo build: 'false' on this job.
  • rake test:integration never gated a PR. Now its own Asset Pipeline job.

3. Two schema test files were 100% commented out

breadcrumb_schema_test.rb and service_schema_test.rb had zero active tests behind stale TODO: Restore when <X> schema implemented in reverted HTML markers. The build emits both today. Uncommented, 3 tests each.

A test file existing is not coverage — grep for def test_, not for the filename.

4. The lead-gen forms had no behavioral test

contact-form.html renders every field's name from [params.forms.contact]. Hugo renders a missing param as "" with no error, the page stays pixel-identical to the screenshot baseline, and leads submit blank.

New test/unit/lead_forms_test.rb. RED verified, not assumed — renaming first_name in hugo.toml:

contact-us/index.html: these fields render name="" and submit nothing.
Expected ["input_5_1"] to be empty.

5. FAQ structured data had no test

New faq_schema_test.rb (6 tests), including a sweep asserting every service page declaring faqs emits the schema — so a template guard that stops matching turns red instead of silently dropping rich results across the section.

6. Conditional-assertion guards — all 61 triaged

if el ... assert ... end passes when the element is absent. Every candidate was checked against the built page: element present → presence assertion; element absent → dead branch, deleted with an in-place note on what to assert if the feature ships.

This turned up four live bugs the guards were hiding:

  1. single_template_test.rb never tested a single page. @test_pages led with "blog/index.html" and .first picked it, so 376 lines nominally covering single.html ran against the list page. Retargeting it to a real post immediately exposed bug 4.
  2. Two item selectors omitted .blog-post — the only class the blog index renders. test_list_page_date_information and test_blog_post_partials matched zero items on every run since they were written.
  3. css_urls.any? do |url| assert ... endany? short-circuits on the first truthy block result and assert returns true, so only the first stylesheet was ever checked.
  4. An over-strict a11y rule flagged image-only links as having no accessible name. A link wrapping an image takes its name from the image's alt (WCAG 2.1 SC 1.1.1). Fixed the rule, not the markup.

Skip-style guards were the same defect one level up and are gone too: list_template skipped 13 tests with no list page; 404_template skipped 12 with no 404.html; template_cleanup_validation had 9 next unless test_page_exists? + 3 return unless; seo_schema called skip "Schema N is empty - might indicate template issue" on exactly the template issue it exists to catch.

7. CI timeouts sized from the measured worst case

Running the new gates produced real numbers. Broken Internal Links: 3.5 min warm, 10.7 min cold (right after master moved and invalidated resources/_gen) — at its original 10-min cap that run would have gone red for nothing but cache state. Asset Pipeline: ~10 min for the suite, plus an actions/checkout measured at 7 min on one slow runner = 17 min against a 15-min cap.

Raised to 15 and 25. A gate that flakes on timeout teaches reviewers to ignore red, which costs more than the runner minutes.

Also

.okf/build/ci-gates.md claimed a toolchain drift test "fails the build when any copy diverges" — that test was deleted 2026-08-01, so the pins are convention-only with zero enforcement. Corrected, plus the same stale phrase in the Rakefile :guards comment.

Still open (documented, not in this PR)

Validator ratchet for blog/marketing content (course 0/82 em-dash files vs blog 208/607 — the rule holds exactly where a validator enforces it); DOM tests for the 4 page templates no test parses; static/sw.js; lib/sync/sources/sanity.rb (129 lines, referenced by nothing) delete-or-test; bin/qtest pages_for mapping.

Testing

  • rake test:unit — 275 runs, 6096 assertions, 0 failures
  • rake test:integration — 11 runs, 0 failures, and confirmed to fail loudly on a broken build
  • rake test:guards — 5 runs, 0 failures
  • CI green on the merged head: Unit Tests, build, Broken Internal Links, Asset Pipeline

Visual suites deliberately not run. The diff touches no themes/, layouts/, assets/, *.css, or content bodies — only test/, Rakefile, .github/workflows/, docs/, and .okf/. Per CLAUDE.md the bin/test + bin/dtest pair gates changes to visual surfaces; there are none here.

One environment note: parse_html_file uses bare File.read, so under a US-ASCII default encoding (no LANG) Nokogiri aborts and 73 template tests fail for a non-template reason. Run under LANG=C.UTF-8.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fza9okyuQ5QdXesVQ85n3b

Audits what the suite covers per layer and ranks the gaps.

Measured: lib/ is at 91.6% line coverage (716/782), so the risk is not
in the Ruby. It is in the layers the suite treats as rendered output,
plus two mechanisms that report green without testing anything:

- test/integration/hugo_pipeline_test.rb skips the whole asset-pipeline
  suite when the Hugo build fails, which is the failure it guards.
- 42 conditional assertion guards (`if el ... assert ... end`) pass when
  the element is absent.

Also found: rake test:html_proofer is invoked by no workflow, hook, or
script; rake test:integration never gates a PR; the contact and
free-consultation form field names come from site params that Hugo
renders as "" when missing, with no test reading the DOM; FAQ schema
ships on 10 service pages untested while its four sibling schemas each
have a test file.

OKF: .okf/build/ci-gates.md claimed a toolchain drift test "fails the
build when any copy diverges" - that test was deleted on 2026-08-01, so
the pins are synced by convention with zero enforcement. Corrected, and
the same stale phrase dropped from the Rakefile :guards comment.

Analysis and proposals only; no test changes in this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fza9okyuQ5QdXesVQ85n3b
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@pftg, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e93b3aae-6824-4218-badd-118fa59dd6ef

📥 Commits

Reviewing files that changed from the base of the PR and between 24262e0 and bcb4323.

📒 Files selected for processing (24)
  • .github/workflows/link-check.yml
  • .github/workflows/publish.yml
  • .okf/build/ci-gates.md
  • .okf/build/index.md
  • .okf/log.md
  • Rakefile
  • docs/20-29-testing-qa/20.10-test-coverage-gap-analysis-reference.md
  • docs/20-29-testing-qa/README.md
  • test/integration/hugo_pipeline_test.rb
  • test/unit/404_template_test.rb
  • test/unit/asset_url_validation_test.rb
  • test/unit/baseof_template_test.rb
  • test/unit/home_template_test.rb
  • test/unit/hugo_asset_validation_test.rb
  • test/unit/hugo_partials_test.rb
  • test/unit/lead_forms_test.rb
  • test/unit/list_template_test.rb
  • test/unit/meta_tags/breadcrumb_schema_test.rb
  • test/unit/meta_tags/faq_schema_test.rb
  • test/unit/meta_tags/seo_schema_test.rb
  • test/unit/meta_tags/service_schema_test.rb
  • test/unit/single_template_test.rb
  • test/unit/template_cleanup_validation_test.rb
  • test/unit/testimonial_shortcode_test.rb

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Acts on items 1-4 of the gap analysis in the previous commit.
rake test:unit goes 272 -> 285 runs, 5723 -> 5935 assertions, 0 failures.

Integration suite fails loudly. hugo_pipeline_test replaces its two
*_ready? predicates with build_failure, flunked in setup: it separates
"hugo not on PATH" from a real build error and prints the last 30 lines
of build output. Verified by shimming hugo to /bin/false - the suite
fails instead of skipping 11 tests into a green report. The two
"No * CSS file found" skips became refute_nil.

Two dead CI gates wired. rake test:integration now runs on PRs as its
own Asset Pipeline job, separate from unit_tests because it drives two
Hugo builds of its own. rake test:html_proofer - previously invoked by
no workflow, hook, or script - joins test:links in one rake invocation;
build_for_linkcheck is memoized per process so the pair shares a single
production build, since two steps would build the site twice and blow
the job timeout that already forced setup-hugo build: 'false'.

Two schema test files were dead code. breadcrumb_schema_test and
service_schema_test were commented out in full behind stale "restore
when <X> schema implemented in reverted HTML" TODOs; the build emits
both today. Uncommented, 3 tests each. A test file existing is not
coverage.

New tests for two untested paths. lead_forms_test asserts a non-empty
absolute action and a non-empty name on every field of the contact,
free-consultation, and career forms - Hugo renders a missing
[params.forms.*] key as "" without failing the build, and the
screenshot baselines cannot see it. RED-verified by renaming first_name
in hugo.toml: the failure names the field id and the config key.
faq_schema_test covers the FAQPage block on the service pages,
including a sweep asserting every page declaring faqs emits it.

Guard sweep, baseof and 404. Presence assertions where the element
exists; dead branches removed where it does not - .logo-image-main is
gone from the codebase, there is no meta[name=referrer], and the site
has no search form. The mermaid SRI test asserted the retired
jsdelivr+SRI implementation while running against index.html, which
never loads mermaid; retargeted to a diagram page and rewritten to
assert same-origin. Two 404 tests only ever called puts "INFO: ...",
and one evaluated external_scripts.length and discarded it - those now
assert invariants that hold (the 404 page pulls zero third-party
scripts or stylesheets). ~41 guard candidates remain in 5 files,
enumerated in the doc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fza9okyuQ5QdXesVQ85n3b
@pftg pftg changed the title docs: test coverage gap analysis + correct stale CI drift-gate claim test: close the false-green mechanisms, wire the dead CI gates (+ coverage gap analysis) Aug 7, 2026
claude added 5 commits August 7, 2026 18:31
rake test:unit: 285 -> 276 runs, 5935 -> 5967 assertions, 0 failures.
Fewer tests, more assertions - 9 removed tests could not fail, and the
survivors gained real presence checks.

list_template_test found a live bug the guard was hiding.
test_list_page_date_information matched items with
"article, .post, .post-item, .entry" - a list omitting `.blog-post`,
which is the ONLY class the blog index renders. It selected zero items
on every run and `if items.any?` made that read as a pass. Both tests
now share ITEM_SELECTOR so the two cannot drift again.

Promoted to real assertions where the build has the element: post items
(a blog index that lists nothing is this page's headline failure),
pagination, per-item dates, heading hierarchy, homepage Organization
schema, homepage social links, preload tags.

Removed where the site has no such feature, so no assertion ever ran:
list-page filtering/sorting, RSS head link, search, breadcrumb elements
(the JSON-LD BreadcrumbList it does emit is covered by
breadcrumb_schema_test), author and category/tag elements, homepage
breadcrumbs, homepage CSP meta, homepage analytics (environment-gated
out of the test build). Each removal is recorded in place with what to
assert if the feature ships.

Replaced discarded-value lines with the invariant behind them: the blog
index and homepage both load zero third-party scripts and stylesheets,
which is why they need no dns-prefetch. The old code computed
`external_scripts.length + external_stylesheets.length` and threw it
away.

setup no longer skips. list_template skipped all 13 tests when no list
page existed; the blog index vanishing is the regression, not a reason
to report green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fza9okyuQ5QdXesVQ85n3b
Records the live bug the guard was hiding (list_template's item selector
omitted .blog-post, the only class the blog index renders, so it matched
zero items on every run), the classification rule that made the sweep
tractable, and the accurate remaining count: 32 of 61 candidates
addressed, ~29 left in 3 files plus singletons.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fza9okyuQ5QdXesVQ85n3b
rake test:unit: 275 runs / 6086 assertions / 0 failures, from the 272 /
5723 baseline. Assertions up 363 while the test count dropped by 13.

Four bugs the guards were hiding. A guard does not just miss future
regressions, it hides present ones:

1. single_template_test.rb never tested a single page. @test_pages led
   with "blog/index.html" and .first picked it, so 376 lines nominally
   covering single.html ran against the LIST page. Pinned to a real post
   via SINGLE_PAGE - which immediately exposed bug 4.
2. Two item selectors omitted .blog-post, the only class the blog index
   renders, so test_list_page_date_information and
   test_blog_post_partials matched zero items on every run.
3. asset_url_validation used `css_urls.any? do |url| assert ... end`.
   any? short-circuits on the first truthy block result and assert
   returns true, so only the FIRST stylesheet was ever checked.
4. An over-strict a11y rule flagged image-only links as having no
   accessible name. A link wrapping an image takes its name from the
   image alt (WCAG 2.1 SC 1.1.1); blog posts wrap YouTube thumbnails
   this way. Fixed the rule, not the markup.

Skip-style guards are the same defect one level up and are gone too:
list_template skipped 13 tests with no list page, 404_template skipped
12 with no 404.html, template_cleanup_validation carried 9
`next unless test_page_exists?` plus 3 `return unless` (now one
assert_empty in setup, helper deleted), and seo_schema called
skip "Schema N is empty - might indicate template issue" on exactly the
template issue it exists to catch.

Retargeted rather than deleted where a partial renders elsewhere:
hugo_partials' social-sharing test checked index and about-us, neither
of which renders blog/share.html, so it now uses a post.

Method: check the built page first, then decide. Element present ->
presence assertion. Element absent -> dead branch, delete it and record
in place what to assert if the feature ships.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fza9okyuQ5QdXesVQ85n3b
…e-analysis-5zm7nj

# Conflicts:
#	.okf/log.md
Three runs of the new gates gave real numbers. Broken Internal Links
took 3.5 min warm and 10.7 min cold, right after master moved and
invalidated resources/_gen - at its original 10-minute cap that run
would have gone red for nothing but cache state (the raise to 15 in the
previous commit already covers it).

Asset Pipeline needs the same treatment. The suite itself is ~10 min:
two full Hugo builds, and the dev-environment one cannot reuse the
production-keyed resource cache, so it reprocesses images. On top of
that, actions/checkout was observed taking 7 minutes on a slow runner -
17 minutes of wall clock against a 15-minute cap. Raised to 25.

Sizing a timeout from the average rather than the worst observed run
buys a gate that flakes, and a gate that flakes teaches reviewers to
ignore red. The runner minutes are not worth that trade.

The per-step timings are the diagnosis tell here: at job level the
7-minute checkout is invisible and reads as a slow test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fza9okyuQ5QdXesVQ85n3b
@pftg
pftg marked this pull request as ready for review August 7, 2026 19:33
@pftg
pftg merged commit 0a35723 into master Aug 7, 2026
5 checks passed
@pftg
pftg deleted the claude/test-coverage-analysis-5zm7nj branch August 7, 2026 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants